package util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Represents the basic message sent between entities. 
 * @author Bitidork
 *
 */
@XmlRootElement(name = "Message")
@XmlAccessorType(XmlAccessType.NONE)
public final class MBMessage {
	
	// %%%%%%%%%%%%%%%%%%%%%
	// Constants
	// %%%%%%%%%%%%%%%%%%%%% 	
	
	/**
	 * Determines whether messages are output in a format that is human readable or not.
	 */
	public final static boolean HUMAN_READABLE_FORMAT = true;
	
	/**
	 * The character encoding to use.
	 */
	public final static String ENCODING = "UTF-8";
	
	// %%%%%%%%%%%%%%%%%%%%%%
	// Constructors
	// %%%%%%%%%%%%%%%%%%%%%%
	
	/**
	 * Creates an MBMessage with the given source, type, and data.
	 * @param source The source of the message, or null if the source is implied.
	 * @param type The type of the message.
	 * @param data The content of the message.
	 */
	private MBMessage( final String source, final String type, final String data ) {
		this.source = source;
		this.type = type;
		this.data = data;
	}
	
	/**
	 * Creates an MBMessage with the given type and data, and null source.
	 * @param type The type of the message.
	 * @param data The content of the message.
	 */
	private MBMessage( final String type, final String data ) {
		this( null, type, data );
	}
	
	/**
	 * Default constructor, necessary for JAXB.
	 */
	private MBMessage( ) {
		this( null, null, null );
	}
	
	// @TODO Finish constructors for other message types; do not let outside classes use the above constructors directly, ever.
	
	// %%%%%%%%%%%%%%%%%%%%%%
	// Reformatting
	// %%%%%%%%%%%%%%%%%%%%%%
	
	public static void main( String[] args ) throws JAXBException {
		lib.init( args );
		MBMessage msg = new MBMessage("src", "type", "data");
		System.out.println( msg.toString() );
		
		MBMessage msg2 = MBMessage.fromString( msg.toString( ) );
		System.out.println( "eq?(msg, msg2): " + (msg.equals(msg2) ) );
	}
	
	/**
	 * Outputs this message to the given stream.
	 * @param os The output stream.
	 * @throws JAXBException if the context object could not be created or if there was a marshalling error.
	 * @throws IllegalArgumentException if the OutputStream parameter is null.
	 */
	private void toStream( final OutputStream os ) throws JAXBException {
		JAXBContext jc = MBMessage.getJAXBContext( );
		
		// create a marshaller; JAXBContext is threadsafe but marshaller is not
		Marshaller m = jc.createMarshaller( );
		m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, MBMessage.HUMAN_READABLE_FORMAT );
		m.setProperty( Marshaller.JAXB_ENCODING, MBMessage.ENCODING );
		m.marshal( this, os );
	}
	
	/**
	 * Reads a message from the supplied input stream.
	 * @param is The input stream.
	 * @return The message object read from the input stream.
	 * @throws JAXBException if the context object could not be created or if there was a marshalling error.
	 * @throws IllegalArgumentException if the InputStream parameter is null.
	 */
	private static MBMessage fromStream( final InputStream is ) throws JAXBException {
		JAXBContext jc = MBMessage.getJAXBContext( );
		
		// create unmarshaller
		Unmarshaller um = jc.createUnmarshaller( );
		return (MBMessage)um.unmarshal( is );
	}
	
	@Override public String toString( ) {
		try {
			return this.toStringEx( );
		} catch (JAXBException e) {
			lib.debug( "The following error was encountered when marshalling an MBMessage: " + e.toString( ) );
			return null;
		} catch (UnsupportedEncodingException e) {
			lib.debug( "Unsupported encoding: " + MBMessage.ENCODING );
			return null;
		}
	}
	
	/**
	 * Gets the string representation of this object, with exceptions ducked to the caller.
	 * @return The string representation of this object. This value is never null.
	 * @throws JAXBException
	 * @throws UnsupportedEncodingException
	 */
	public String toStringEx( ) throws JAXBException, UnsupportedEncodingException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream( );
		this.toStream( baos );
		return baos.toString( MBMessage.ENCODING );
	}
	
	/**
	 * Gets the message object from its string representation.
	 * @param str The string.
	 * @return The parsed message, or null if an error was encountered while unmarshalling the message.
	 */
	public static MBMessage fromString( final String str ) {
		try {
			return MBMessage.fromStringEx( str );
		} catch (JAXBException e) {
			lib.debug( "Error when unmarshalling an MBMessage: " + e.toString( ) );
			return null;
		}
	}
	
	/**
	 * Gets the message object from its string representation, with exceptions ducked to the caller.
	 * @param str The string.
	 * @return The parsed message. This value is never null.
	 */
	public static MBMessage fromStringEx( final String str ) throws JAXBException {
		ByteArrayInputStream bais = new ByteArrayInputStream( str.getBytes( ) );
		return MBMessage.fromStream( bais );
	}
	
	// %%%%%%%%%%%%%%%%%%%%%%
	// Fields and accessors
	// %%%%%%%%%%%%%%%%%%%%%%
	
	/**
	 * The JAXBContext used for this class.
	 * This is initialized in the {@link #toStream(OutputStream)} and {@link MBMessage#fromStream(InputStream)} methods on the first use.
	 */
	private static JAXBContext jc = null;
	
	/**
	 * The original source of the message (who created it), or null if the source is implied.
	 */
	@XmlElement final public String source;
	
	/**
	 * The message type.
	 * This determines the contents of the data field.
	 */
	@XmlElement final public String type;
	
	/**
	 * The message data.
	 * This value can be another {@link #MBMessage}.
	 */
	@XmlElement final public String data;
	
	// %%%%%%%%%%%%%%%%%%%%%%
	// Other methods
	// %%%%%%%%%%%%%%%%%%%%%%
	
	/**
	 * Gets the JAXBContext instance used for this class, or creates a new context if needed.
	 * This method is thread-safe.
	 * @return Returns the JAXBContext instance used for this class.
	 * @throws JAXBException if an error was encountered while creating the JAXBContext.
	 */
	private final static synchronized JAXBContext getJAXBContext( ) throws JAXBException {
		// create context if needed
		if ( jc == null )
			jc = JAXBContext.newInstance( MBMessage.class );
		
		return jc;
	}
	
	@Override
	public boolean equals( final Object obj ) {
		if ( obj == null || !(obj instanceof MBMessage))
			return false;
		final MBMessage msg = (MBMessage)obj;
		
		return ( this.type == null ? msg.type == null : this.type.equals( msg.type ) ) &&
				( this.source == null ? msg.source == null : this.source.equals( msg.source ) ) &&
				( this.data == null ? msg.data == null : this.data.equals( msg.data ) );
	}
}
